/*
 *   Copyright 2021 zzh
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */

package com.aduib.boot.common.util;

import com.aduib.boot.common.exception.IoException;
import com.aduib.boot.common.io.BOMInputStream;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.regex.Pattern;

/**
 * @description: FileUtils
 * @author: zzh
 * @date: 2021/10/28 10:17
 */
public class FileUtils {

  /** 针对ClassPath路径的伪协议前缀（兼容Spring）: "classpath:" */
  public static final String CLASSPATH_URL_PREFIX = "classpath:";
  /** URL 前缀表示文件: "file:" */
  public static final String FILE_URL_PREFIX = "file:";
  /** URL 前缀表示jar: "jar:" */
  public static final String JAR_URL_PREFIX = "jar:";
  /** URL 前缀表示war: "war:" */
  public static final String WAR_URL_PREFIX = "war:";
  /** URL 协议表示文件: "file" */
  public static final String URL_PROTOCOL_FILE = "file";
  /** URL 协议表示Jar文件: "jar" */
  public static final String URL_PROTOCOL_JAR = "jar";
  /** URL 协议表示zip文件: "zip" */
  public static final String URL_PROTOCOL_ZIP = "zip";

  /** .java文件扩展名 */
  public static final String EXT_JAVA = ".java";
  /** .class文件扩展名 */
  public static final String EXT_CLASS = ".class";
  /** .jar文件扩展名 */
  public static final String EXT_JAR = ".jar";
  /** 类Unix路径分隔符 */
  public static final char UNIX_SEPARATOR = CharUtils.SLASH;
  /** Windows路径分隔符 */
  public static final char WINDOWS_SEPARATOR = CharUtils.BACKSLASH;

  /** Windows下文件名中的无效字符 */
  private static final Pattern FILE_NAME_INVALID_PATTERN_WIN = Pattern.compile("[\\\\/:*?\"<>|]");

  /**
   * 获取路径分隔符
   * @return
   */
  public static String getSeparator(){
    if(SystemUtils.INSTANCE.isWindows()){
      return String.valueOf(WINDOWS_SEPARATOR);
    }else{
      return String.valueOf(UNIX_SEPARATOR);
    }
  }
  /**
   * 是否为Windows环境
   *
   * @return 是否为Windows环境
   */
  public static boolean isWindows() {
    return WINDOWS_SEPARATOR == File.separatorChar;
  }

  /**
   * 列出目录文件<br>
   * 给定的绝对路径不能是压缩包中的路径
   *
   * @param path 目录绝对路径或者相对路径
   * @return 文件列表（包含目录）
   */
  public static File[] ls(String path) {
    if (path == null) {
      return null;
    }

    File file = file(path);
    if (file.isDirectory()) {
      return file.listFiles();
    }
    throw new IoException(StrUtils.format("Path [{}] is not directory!", path));
  }

  /**
   * 文件是否为空<br>
   * 目录：里面没有文件时为空 文件：文件大小为0时为空
   *
   * @param file 文件
   * @return 是否为空，当提供非目录时，返回false
   */
  public static boolean isEmpty(File file) {
    if (null == file) {
      return true;
    }

    if (file.isDirectory()) {
      String[] subFiles = file.list();
      return ArrayUtils.isEmpty(subFiles);
    } else if (file.isFile()) {
      return file.length() <= 0;
    }

    return false;
  }

  /**
   * 目录是否为空
   *
   * @param file 目录
   * @return 是否为空，当提供非目录时，返回false
   */
  public static boolean isNotEmpty(File file) {
    return false == isEmpty(file);
  }

  /**
   * 目录是否为空
   *
   * @param dir 目录
   * @return 是否为空
   */
  public static boolean isDirEmpty(File dir) {
    return isDirEmpty(dir.toPath());
  }

  /**
   * 递归遍历目录以及子目录中的所有文件<br>
   * 如果提供file为文件，直接返回过滤结果
   *
   * @param path 当前遍历文件或目录的路径
   * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录
   * @return 文件列表
   */
  public static List<File> loopFiles(String path, FileFilter fileFilter) {
    return loopFiles(file(path), fileFilter);
  }

  /**
   * 递归遍历目录以及子目录中的所有文件<br>
   * 如果提供file为文件，直接返回过滤结果
   *
   * @param file 当前遍历文件或目录
   * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录
   * @return 文件列表
   */
  public static List<File> loopFiles(File file, FileFilter fileFilter) {
    return loopFiles(file, -1, fileFilter);
  }

  /**
   * 递归遍历目录并处理目录下的文件，可以处理目录或文件：
   *
   * <ul>
   *   <li>非目录则直接调用{@link Consumer}处理
   *   <li>目录则递归调用此方法处理
   * </ul>
   *
   * @param file 文件或目录，文件直接处理
   * @param consumer 文件处理器，只会处理文件
   */
  public static void walkFiles(File file, Consumer<File> consumer) {
    if (file.isDirectory()) {
      final File[] subFiles = file.listFiles();
      if (ArrayUtils.isNotEmpty(subFiles)) {
        for (File tmp : subFiles) {
          walkFiles(tmp, consumer);
        }
      }
    } else {
      consumer.accept(file);
    }
  }

  /**
   * 递归遍历目录以及子目录中的所有文件<br>
   * 如果提供file为文件，直接返回过滤结果
   *
   * @param file 当前遍历文件或目录
   * @param maxDepth 遍历最大深度，-1表示遍历到没有目录为止
   * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录，null表示接收全部文件
   * @return 文件列表
   */
  public static List<File> loopFiles(File file, int maxDepth, FileFilter fileFilter) {
    return loopFiles(file.toPath(), maxDepth, fileFilter);
  }

  /**
   * 递归遍历目录以及子目录中的所有文件
   *
   * @param path 当前遍历文件或目录的路径
   * @return 文件列表
   */
  public static List<File> loopFiles(String path) {
    return loopFiles(file(path));
  }

  /**
   * 递归遍历目录以及子目录中的所有文件
   *
   * @param file 当前遍历文件
   * @return 文件列表
   */
  public static List<File> loopFiles(File file) {
    return loopFiles(file, null);
  }

  /**
   * 目录是否为空
   *
   * @param dirPath 目录
   * @return 是否为空
   * @throws IoException IOException
   */
  public static boolean isDirEmpty(Path dirPath) {
    try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath)) {
      return false == dirStream.iterator().hasNext();
    } catch (IOException e) {
      throw new IoException(e);
    }
  }

  /**
   * 递归遍历目录以及子目录中的所有文件<br>
   * 如果提供path为文件，直接返回过滤结果
   *
   * @param path 当前遍历文件或目录
   * @param maxDepth 遍历最大深度，-1表示遍历到没有目录为止
   * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录，null表示接收全部文件
   * @return 文件列表
   */
  public static List<File> loopFiles(Path path, int maxDepth, FileFilter fileFilter) {
    final List<File> fileList = new ArrayList<>();

    if (null == path || false == Files.exists(path)) {
      return fileList;
    } else if (false == isDirectory(path)) {
      final File file = path.toFile();
      if (null == fileFilter || fileFilter.accept(file)) {
        fileList.add(file);
      }
      return fileList;
    }

    walkFiles(
        path,
        maxDepth,
        new SimpleFileVisitor<Path>() {

          @Override
          public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
            final File file = path.toFile();
            if (null == fileFilter || fileFilter.accept(file)) {
              fileList.add(file);
            }
            return FileVisitResult.CONTINUE;
          }
        });

    return fileList;
  }

  /**
   * 遍历指定path下的文件并做处理
   *
   * @param start 起始路径，必须为目录
   * @param visitor {@link FileVisitor} 接口，用于自定义在访问文件时，访问目录前后等节点做的操作
   * @see Files#walkFileTree(Path, java.util.Set, int, FileVisitor)
   */
  public static void walkFiles(Path start, FileVisitor<? super Path> visitor) {
    walkFiles(start, -1, visitor);
  }

  /**
   * 遍历指定path下的文件并做处理
   *
   * @param start 起始路径，必须为目录
   * @param maxDepth 最大遍历深度，-1表示不限制深度
   * @param visitor {@link FileVisitor} 接口，用于自定义在访问文件时，访问目录前后等节点做的操作
   * @see Files#walkFileTree(Path, java.util.Set, int, FileVisitor)
   */
  public static void walkFiles(Path start, int maxDepth, FileVisitor<? super Path> visitor) {
    if (maxDepth < 0) {
      // < 0 表示遍历到最底层
      maxDepth = Integer.MAX_VALUE;
    }

    try {
      Files.walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), maxDepth, visitor);
    } catch (IOException e) {
      throw new IoException(e);
    }
  }

  /**
   * 判断是否为目录，如果file为null，则返回false<br>
   * 此方法不会追踪到软链对应的真实地址，即软链被当作文件
   *
   * @param path {@link Path}
   * @return 如果为目录true
   */
  public static boolean isDirectory(Path path) {
    return isDirectory(path, false);
  }

  /**
   * 判断是否为目录，如果file为null，则返回false
   *
   * @param path {@link Path}
   * @param isFollowLinks 是否追踪到软链对应的真实地址
   * @return 如果为目录true
   */
  public static boolean isDirectory(Path path, boolean isFollowLinks) {
    if (null == path) {
      return false;
    }
    final LinkOption[] options =
        isFollowLinks ? new LinkOption[0] : new LinkOption[] {LinkOption.NOFOLLOW_LINKS};
    return Files.isDirectory(path, options);
  }

  /**
   * 将流的内容写入文件<br>
   * 此方法会自动关闭输入流
   *
   * @param dest 目标文件
   * @param in 输入流
   * @return dest
   * @throws IoException IO异常
   */
  public static File writeFromStream(InputStream in, File dest) throws IoException {
    return writeFromStream(in, dest, true);
  }

  /**
   * 将流的内容写入文件
   *
   * @param file 目标文件
   * @param in 输入流
   * @param isCloseIn 是否关闭输入流
   * @return dest
   * @throws IoException IO异常
   */
  public static File writeFromStream(InputStream in, File file, boolean isCloseIn)
      throws IoException {
    FileOutputStream out = null;
    try {
      out = new FileOutputStream(touch(file));
      IOUtils.copy(in, out);
    } catch (IOException e) {
      throw new IoException(e);
    } finally {
      IOUtils.close(out);
      if (isCloseIn) {
        IOUtils.close(in);
      }
    }
    return file;
  }

  /**
   * 将流的内容写入文件<br>
   * 此方法会自动关闭输入流
   *
   * @param in 输入流
   * @param fullFilePath 文件绝对路径
   * @return 目标文件
   * @throws IoException IO异常
   */
  public static File writeFromStream(InputStream in, String fullFilePath) throws IoException {
    return writeFromStream(in, touch(fullFilePath));
  }

  /**
   * 创建文件及其父目录，如果这个文件存在，直接返回这个文件<br>
   * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型
   *
   * @param parent 父文件对象
   * @param path 文件路径
   * @return File
   * @throws IoException IO异常
   */
  public static File touch(File parent, String path) throws IoException {
    return touch(file(parent, path));
  }

  /**
   * 创建文件及其父目录，如果这个文件存在，直接返回这个文件<br>
   * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型
   *
   * @param parent 父文件对象
   * @param path 文件路径
   * @return File
   * @throws IoException IO异常
   */
  public static File touch(String parent, String path) throws IoException {
    return touch(file(parent, path));
  }

  /**
   * 创建所给文件或目录的父目录
   *
   * @param file 文件或目录
   * @return 父目录
   */
  //    public static File mkParentDirs(File file) {
  //        if (null == file) {
  //            return null;
  //        }
  //        return mkdir(file.getParentFile());
  //    }

  /**
   * 创建父文件夹，如果存在直接返回此文件夹
   *
   * @param path 文件夹路径，使用POSIX格式，无论哪个平台
   * @return 创建的目录
   */
  //    public static File mkParentDirs(String path) {
  //        if (path == null) {
  //            return null;
  //        }
  //        return mkParentDirs(file(path));
  //    }

  // ------------------------------------//

  /**
   * 创建File对象，相当于调用new File()，不做任何处理
   *
   * @param path 文件路径
   * @return File
   */
  public static File newFile(String path) {
    return new File(path);
  }

  /**
   * 创建File对象，自动识别相对或绝对路径，相对路径将自动从ClassPath下寻找
   *
   * @param path 文件路径
   * @return File
   */
  public static File file(String path) {
    if (null == path) {
      return null;
    }
    return new File(getAbsolutePath(path));
  }

  /**
   * 创建File对象<br>
   * 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/
   *
   * @param parent 父目录
   * @param path 文件路径
   * @return File
   */
  //    public static File file(String parent, String path) {
  //        return file(new File(parent), path);
  //    }

  /**
   * 创建File对象<br>
   * 根据的路径构建文件，在Win下直接构建，在Linux下拆分路径单独构建 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/
   *
   * @param parent 父文件对象
   * @param path 文件路径
   * @return File
   */
  public static File file(File parent, String path) {
    if (StrUtils.isBlank(path)) {
      throw new NullPointerException("File path is blank!");
    }
    return checkSlip(parent, buildFile(parent, path));
  }

  /**
   * 通过多层目录参数创建文件<br>
   * 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/
   *
   * @param directory 父目录
   * @param names 元素名（多层目录名），由外到内依次传入
   * @return the file 文件
   */
  public static File file(File directory, String... names) {
    if (ArrayUtils.isEmpty(names)) {
      return directory;
    }

    File file = directory;
    for (String name : names) {
      if (null != name) {
        file = file(file, name);
      }
    }
    return file;
  }

  /**
   * 通过多层目录创建文件
   *
   * <p>元素名（多层目录名）
   *
   * @param names 多层文件的文件名，由外到内依次传入
   * @return the file 文件
   */
  public static File file(String... names) {
    if (ArrayUtils.isEmpty(names)) {
      return null;
    }

    File file = null;
    for (String name : names) {
      if (file == null) {
        file = file(name);
      } else {
        file = file(file, name);
      }
    }
    return file;
  }

  /**
   * 创建File对象
   *
   * @param uri 文件URI
   * @return File
   */
  public static File file(URI uri) {
    if (uri == null) {
      throw new NullPointerException("File uri is null!");
    }
    return new File(uri);
  }

  /**
   * 创建File对象
   *
   * @param url 文件URL
   * @return File
   */
  public static File file(URL url) {
    return new File(URLUtils.toURI(url));
  }

  /**
   * 获取临时文件路径（绝对路径）
   *
   * @return 临时文件路径
   */
  public static String getTmpDirPath() {
    return System.getProperty("java.io.tmpdir");
  }

  /**
   * 获取临时文件目录
   *
   * @return 临时文件目录
   */
  public static File getTmpDir() {
    return file(getTmpDirPath());
  }

  /**
   * 获取用户路径（绝对路径）
   *
   * @return 用户路径
   */
  public static String getUserHomePath() {
    return System.getProperty("user.home");
  }

  /**
   * 获取用户目录
   *
   * @return 用户目录
   */
  public static File getUserHomeDir() {
    return file(getUserHomePath());
  }

  // ----------------------------------------------//

  /**
   * 创建文件及其父目录，如果这个文件存在，直接返回这个文件<br>
   * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型
   *
   * @param fullFilePath 文件的全路径，使用POSIX风格
   * @return 文件，若路径为null，返回null
   * @throws IoException IO异常
   */
  public static File touch(String fullFilePath) throws IoException {
    if (fullFilePath == null) {
      return null;
    }
    return touch(file(fullFilePath));
  }

  /**
   * 创建文件及其父目录，如果这个文件存在，直接返回这个文件<br>
   * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型
   *
   * @param file 文件对象
   * @return 文件，若路径为null，返回null
   * @throws IoException IO异常
   */
  public static File touch(File file) throws IoException {
    if (null == file) {
      return null;
    }
    if (false == file.exists()) {
      mkParentDirs(file);
      try {
        //noinspection ResultOfMethodCallIgnored
        file.createNewFile();
      } catch (Exception e) {
        throw new IoException(e);
      }
    }
    return file;
  }

  /**
   * 创建所给文件或目录的父目录
   *
   * @param file 文件或目录
   * @return 父目录
   */
  public static File mkParentDirs(File file) {
    if (null == file) {
      return null;
    }
    return mkdir(file.getParentFile());
  }

  /**
   * 创建父文件夹，如果存在直接返回此文件夹
   *
   * @param path 文件夹路径，使用POSIX格式，无论哪个平台
   * @return 创建的目录
   */
  public static File mkParentDirs(String path) {
    if (path == null) {
      return null;
    }
    return mkParentDirs(file(path));
  }

  /**
   * 创建文件夹，如果存在直接返回此文件夹<br>
   * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型
   *
   * @param dirPath 文件夹路径，使用POSIX格式，无论哪个平台
   * @return 创建的目录
   */
  public static File mkdir(String dirPath) {
    if (dirPath == null) {
      return null;
    }
    final File dir = file(dirPath);
    return mkdir(dir);
  }

  /**
   * 创建文件夹，会递归自动创建其不存在的父文件夹，如果存在直接返回此文件夹<br>
   * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型
   *
   * @param dir 目录
   * @return 创建的目录
   */
  public static File mkdir(File dir) {
    if (dir == null) {
      return null;
    }
    if (false == dir.exists()) {
      //noinspection ResultOfMethodCallIgnored
      dir.mkdirs();
    }
    return dir;
  }

  /**
   * 创建临时文件<br>
   * 创建后的文件名为 prefix[Randon].tmp
   *
   * @param dir 临时文件创建的所在目录
   * @return 临时文件
   * @throws IoException IO异常
   */
  public static File createTempFile(File dir) throws IoException {
    return createTempFile("aduib", null, dir, true);
  }

  /**
   * 创建临时文件<br>
   * 创建后的文件名为 prefix[Randon].tmp
   *
   * @param dir 临时文件创建的所在目录
   * @param isReCreat 是否重新创建文件（删掉原来的，创建新的）
   * @return 临时文件
   * @throws IoException IO异常
   */
  public static File createTempFile(File dir, boolean isReCreat) throws IoException {
    return createTempFile("aduib", null, dir, isReCreat);
  }

  /**
   * 创建临时文件<br>
   * 创建后的文件名为 prefix[Randon].suffix From com.jodd.io.FileUtil
   *
   * @param prefix 前缀，至少3个字符
   * @param suffix 后缀，如果null则使用默认.tmp
   * @param dir 临时文件创建的所在目录
   * @param isReCreat 是否重新创建文件（删掉原来的，创建新的）
   * @return 临时文件
   * @throws IoException IO异常
   */
  public static File createTempFile(String prefix, String suffix, File dir, boolean isReCreat)
      throws IoException {
    int exceptionsCount = 0;
    while (true) {
      try {
        File file = File.createTempFile(prefix, suffix, mkdir(dir)).getCanonicalFile();
        if (isReCreat) {
          //noinspection ResultOfMethodCallIgnored
          file.delete();
          //noinspection ResultOfMethodCallIgnored
          file.createNewFile();
        }
        return file;
      } catch (
          IOException ioex) { // fixes java.io.WinNTFileSystem.createFileExclusively access denied
        if (++exceptionsCount >= 50) {
          throw new IoException(ioex);
        }
      }
    }
  }

  // -------------------------------------//

  /**
   * 修复路径<br>
   * 如果原路径尾部有分隔符，则保留为标准分隔符（/），否则不保留
   *
   * <ol>
   *   <li>1. 统一用 /
   *   <li>2. 多个 / 转换为一个 /
   *   <li>3. 去除两边空格
   *   <li>4. .. 和 . 转换为绝对路径，当..多于已有路径时，直接返回根路径
   * </ol>
   *
   * <p>栗子：
   *
   * <pre>
   * "/foo//" =》 "/foo/"
   * "/foo/./" =》 "/foo/"
   * "/foo/../bar" =》 "/bar"
   * "/foo/../bar/" =》 "/bar/"
   * "/foo/../bar/../baz" =》 "/baz"
   * "/../" =》 "/"
   * "foo/bar/.." =》 "foo"
   * "foo/../bar" =》 "bar"
   * "foo/../../bar" =》 "bar"
   * "//server/foo/../bar" =》 "/server/bar"
   * "//server/../bar" =》 "/bar"
   * "C:\\foo\\..\\bar" =》 "C:/bar"
   * "C:\\..\\bar" =》 "C:/bar"
   * "~/foo/../bar/" =》 "~/bar/"
   * "~/../bar" =》 "bar"
   * </pre>
   *
   * @param path 原路径
   * @return 修复后的路径
   */
  public static String normalize(String path) {
    if (path == null) {
      return null;
    }

    // 兼容Spring风格的ClassPath路径，去除前缀，不区分大小写
    String pathToUse = StrUtils.removePrefixIgnoreCase(path, FileUtils.CLASSPATH_URL_PREFIX);
    // 去除file:前缀
    pathToUse = StrUtils.removePrefixIgnoreCase(pathToUse, FileUtils.FILE_URL_PREFIX);

    // 识别home目录形式，并转换为绝对路径
    if (pathToUse.startsWith("~")) {
      pathToUse = pathToUse.replace("~", getUserHomePath());
    }

    // 统一使用斜杠
    pathToUse = pathToUse.replaceAll("[/\\\\]+", StrUtils.SLASH).trim();
    // 兼容Windows下的共享目录路径（原始路径如果以\\开头，则保留这种路径）
    if (path.startsWith("\\\\")) {
      pathToUse = "\\" + pathToUse;
    }

    String prefix = "";
    int prefixIndex = pathToUse.indexOf(StrUtils.COLON);
    if (prefixIndex > -1) {
      // 可能Windows风格路径
      prefix = pathToUse.substring(0, prefixIndex + 1);
      if (StrUtils.startWith(prefix, StrUtils.C_SLASH)) {
        // 去除类似于/C:这类路径开头的斜杠
        prefix = prefix.substring(1);
      }
      if (false == prefix.contains(StrUtils.SLASH)) {
        pathToUse = pathToUse.substring(prefixIndex + 1);
      } else {
        // 如果前缀中包含/,说明非Windows风格path
        prefix = StrUtils.EMPTY;
      }
    }
    if (pathToUse.startsWith(StrUtils.SLASH)) {
      prefix += StrUtils.SLASH;
      pathToUse = pathToUse.substring(1);
    }

    List<String> pathList = StrUtils.split(pathToUse, String.valueOf(StrUtils.C_SLASH));
    List<String> pathElements = new LinkedList<>();
    int tops = 0;

    String element;
    for (int i = pathList.size() - 1; i >= 0; i--) {
      element = pathList.get(i);
      // 只处理非.的目录，即只处理非当前目录
      if (false == StrUtils.DOT.equals(element)) {
        if (StrUtils.DOUBLE_DOT.equals(element)) {
          tops++;
        } else {
          if (tops > 0) {
            // 有上级目录标记时按照个数依次跳过
            tops--;
          } else {
            // Normal path element found.
            pathElements.add(0, element);
          }
        }
      }
    }
    return prefix + CollectionUtils.join(pathElements,StrUtils.SLASH);
  }

  /**
   * 获取规范的绝对路径
   *
   * @param file 文件
   * @return 规范绝对路径，如果传入file为null，返回null
   */
  public static String getCanonicalPath(File file) {
    if (null == file) {
      return null;
    }
    try {
      return file.getCanonicalPath();
    } catch (IOException e) {
      throw new IoException(e);
    }
  }

  /**
   * 获取绝对路径<br>
   * 此方法不会判定给定路径是否有效（文件或目录存在）
   *
   * @param path 相对路径
   * @param baseClass 相对路径所相对的类
   * @return 绝对路径
   */
  public static String getAbsolutePath(String path, Class<?> baseClass) {
    String normalPath;
    if (path == null) {
      normalPath = StrUtils.EMPTY;
    } else {
      normalPath = normalize(path);
      if (isAbsolutePath(normalPath)) {
        // 给定的路径已经是绝对路径了
        return normalPath;
      }
    }

    // 相对于ClassPath路径
    final URL url = ClassUtils.getResource(normalPath, baseClass);
    if (null != url) {
      // 对于jar中文件包含file:前缀，需要去掉此类前缀，在此做标准化, 解决中文或空格路径被编码的问题
      return normalize(URLUtils.getDecodedPath(url));
    }

    // 如果资源不存在，则返回一个拼接的资源绝对路径
    final String classPath = ClassUtils.getClassFilePath(FileUtils.class);
    if (null == classPath) {
      // throw new NullPointerException("ClassPath is null !");
      // 在jar运行模式中，ClassPath有可能获取不到，此时返回原始相对路径（此时获取的文件为相对工作目录）
      return path;
    }

    // 资源不存在的情况下使用标准化路径有问题，使用原始路径拼接后标准化路径
    return normalize(classPath.concat(Objects.requireNonNull(path)));
  }

  /**
   * 获取绝对路径，相对于ClassPath的目录<br>
   * 如果给定就是绝对路径，则返回原路径，原路径把所有\替换为/<br>
   * 兼容Spring风格的路径表示，例如：classpath:config/example.setting也会被识别后转换
   *
   * @param path 相对路径
   * @return 绝对路径
   */
  public static String getAbsolutePath(String path) {
    return getAbsolutePath(path, null);
  }

  /**
   * 获取标准的绝对路径
   *
   * @param file 文件
   * @return 绝对路径
   */
  public static String getAbsolutePath(File file) {
    if (file == null) {
      return null;
    }

    try {
      return file.getCanonicalPath();
    } catch (IOException e) {
      return file.getAbsolutePath();
    }
  }

  /**
   * 检查父完整路径是否为自路径的前半部分，如果不是说明不是子路径，可能存在slip注入。
   *
   * <p>见http://blog.nsfocus.net/zip-slip-2/
   *
   * @param parentFile 父文件或目录
   * @param file 子文件或目录
   * @return 子文件或目录
   * @throws IllegalArgumentException 检查创建的子文件不在父目录中抛出此异常
   */
  public static File checkSlip(File parentFile, File file) throws IllegalArgumentException {
    if (null != parentFile && null != file) {
      String parentCanonicalPath;
      String canonicalPath;
      try {
        parentCanonicalPath = parentFile.getCanonicalPath();
        canonicalPath = file.getCanonicalPath();
      } catch (IOException e) {
        throw new IoException(e);
      }
      if (false == canonicalPath.startsWith(parentCanonicalPath)) {
        throw new IllegalArgumentException(
            "New file is outside of the parent dir: " + file.getName());
      }
    }
    return file;
  }

  /**
   * 给定路径已经是绝对路径<br>
   * 此方法并没有针对路径做标准化，建议先执行{@link #normalize(String)}方法标准化路径后判断
   *
   * @param path 需要检查的Path
   * @return 是否已经是绝对路径
   */
  public static boolean isAbsolutePath(String path) {
    if (StrUtils.isEmpty(path)) {
      return false;
    }

    // 给定的路径已经是绝对路径了
    return StrUtils.C_SLASH == path.charAt(0) || path.matches("^[a-zA-Z]:([/\\\\].*)?");
  }

  /**
   * 判断是否为目录，如果path为null，则返回false
   *
   * @param path 文件路径
   * @return 如果为目录true
   */
  public static boolean isDirectory(String path) {
    return (null != path) && file(path).isDirectory();
  }

  /**
   * 判断是否为目录，如果file为null，则返回false
   *
   * @param file 文件
   * @return 如果为目录true
   */
  public static boolean isDirectory(File file) {
    return (null != file) && file.isDirectory();
  }

  /**
   * 判断是否为文件，如果path为null，则返回false
   *
   * @param path 文件路径
   * @return 如果为文件true
   */
  public static boolean isFile(String path) {
    return (null != path) && file(path).isFile();
  }

  /**
   * 判断是否为文件，如果file为null，则返回false
   *
   * @param file 文件
   * @return 如果为文件true
   */
  public static boolean isFile(File file) {
    return (null != file) && file.isFile();
  }

  /**
   * 检查两个文件是否是同一个文件<br>
   * 所谓文件相同，是指Path对象是否指向同一个文件或文件夹
   *
   * @param file1 文件1
   * @param file2 文件2
   * @return 是否相同
   * @throws IOUtils IO异常
   * @see Files#isSameFile(Path, Path)
   */
  public static boolean equals(Path file1, Path file2) throws IoException {
    try {
      return Files.isSameFile(file1, file2);
    } catch (IOException e) {
      throw new IoException(e);
    }
  }

  /**
   * 检查两个文件是否是同一个文件<br>
   * 所谓文件相同，是指File对象是否指向同一个文件或文件夹
   *
   * @param file1 文件1
   * @param file2 文件2
   * @return 是否相同
   * @throws IoException IO异常
   */
  public static boolean equals(File file1, File file2) throws IoException {
    if (false == file1.exists() || false == file2.exists()) {
      // 两个文件都不存在判断其路径是否相同， 对于一个存在一个不存在的情况，一定不相同
      return false == file1.exists() //
          && false == file2.exists() //
          && pathEquals(file1, file2);
    }
    return equals(file1.toPath(), file2.toPath());
  }

  /**
   * 比较两个文件内容是否相同<br>
   * 首先比较长度，长度一致再比较内容<br>
   * 此方法来自Apache Commons io
   *
   * @param file1 文件1
   * @param file2 文件2
   * @return 两个文件内容一致返回true，否则false
   * @throws IoException IO异常
   */
  public static boolean contentEquals(File file1, File file2) throws IoException {
    boolean file1Exists = file1.exists();
    if (file1Exists != file2.exists()) {
      return false;
    }

    if (false == file1Exists) {
      // 两个文件都不存在，返回true
      return true;
    }

    if (file1.isDirectory() || file2.isDirectory()) {
      // 不比较目录
      throw new IoException("Can't compare directories, only files");
    }

    if (file1.length() != file2.length()) {
      // 文件长度不同
      return false;
    }

    if (equals(file1, file2)) {
      // 同一个文件
      return true;
    }

    InputStream input1 = null;
    InputStream input2 = null;
    try {
      input1 = getInputStream(file1);
      input2 = getInputStream(file2);
      return IOUtils.contentEquals(input1, input2);

    } finally {
      IOUtils.close(input1);
      IOUtils.close(input2);
    }
  }

  // -----------------------------------------------------------------------

  /**
   * 比较两个文件内容是否相同<br>
   * 首先比较长度，长度一致再比较内容，比较内容采用按行读取，每行比较<br>
   * 此方法来自Apache Commons io
   *
   * @param file1 文件1
   * @param file2 文件2
   * @param charset 编码，null表示使用平台默认编码 两个文件内容一致返回true，否则false
   * @return 是否相同
   * @throws IoException IO异常
   */
  public static boolean contentEqualsIgnoreEOL(File file1, File file2, Charset charset)
      throws IoException {
    boolean file1Exists = file1.exists();
    if (file1Exists != file2.exists()) {
      return false;
    }

    if (!file1Exists) {
      // 两个文件都不存在，返回true
      return true;
    }

    if (file1.isDirectory() || file2.isDirectory()) {
      // 不比较目录
      throw new IoException("Can't compare directories, only files");
    }

    if (equals(file1, file2)) {
      // 同一个文件
      return true;
    }

    Reader input1 = null;
    Reader input2 = null;
    try {
      input1 = getReader(file1, charset);
      input2 = getReader(file2, charset);
      return IOUtils.contentEqualsIgnoreEOL(input1, input2);
    } finally {
      IOUtils.close(input1);
      IOUtils.close(input2);
    }
  }

  /**
   * 文件路径是否相同<br>
   * 取两个文件的绝对路径比较，在Windows下忽略大小写，在Linux下不忽略。
   *
   * @param file1 文件1
   * @param file2 文件2
   * @return 文件路径是否相同
   */
  public static boolean pathEquals(File file1, File file2) {
    if (isWindows()) {
      // Windows环境
      try {
        if (StrUtils.equalsIgnoreCase(file1.getCanonicalPath(), file2.getCanonicalPath())) {
          return true;
        }
      } catch (Exception e) {
        if (StrUtils.equalsIgnoreCase(file1.getAbsolutePath(), file2.getAbsolutePath())) {
          return true;
        }
      }
    } else {
      // 类Unix环境
      try {
        if (StrUtils.equals(file1.getCanonicalPath(), file2.getCanonicalPath())) {
          return true;
        }
      } catch (Exception e) {
        if (StrUtils.equals(file1.getAbsolutePath(), file2.getAbsolutePath())) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * 获得最后一个文件路径分隔符的位置
   *
   * @param filePath 文件路径
   * @return 最后一个文件路径分隔符的位置
   */
  public static int lastIndexOfSeparator(String filePath) {
    if (StrUtils.isNotEmpty(filePath)) {
      int i = filePath.length();
      char c;
      while (--i >= 0) {
        c = filePath.charAt(i);
        if (CharUtils.isFileSeparator(c)) {
          return i;
        }
      }
    }
    return -1;
  }

  /**
   * 判断文件是否被改动<br>
   * 如果文件对象为 null 或者文件不存在，被视为改动
   *
   * @param file 文件对象
   * @param lastModifyTime 上次的改动时间
   * @return 是否被改动
   */
  public static boolean isModifed(File file, long lastModifyTime) {
    if (null == file || false == file.exists()) {
      return true;
    }
    return file.lastModified() != lastModifyTime;
  }

  // -------------------------------------------------------------------------------------------- in
  // start

  /**
   * 获得输入流
   *
   * @param file 文件
   * @return 输入流
   * @throws IoException 文件未找到
   */
  public static BufferedInputStream getInputStream(File file) throws IoException {
    return IOUtils.toBuffered(IOUtils.toStream(file));
  }

  /**
   * 获得输入流
   *
   * @param path 文件路径
   * @return 输入流
   * @throws IoException 文件未找到
   */
  public static BufferedInputStream getInputStream(String path) throws IoException {
    return getInputStream(file(path));
  }

  /**
   * 获得BOM输入流，用于处理带BOM头的文件
   *
   * @param file 文件
   * @return 输入流
   * @throws IoException 文件未找到
   */
  public static BOMInputStream getBOMInputStream(File file) throws IoException {
    try {
      return new BOMInputStream(new FileInputStream(file));
    } catch (IOException e) {
      throw new IoException(e);
    }
  }

  /**
   * 读取带BOM头的文件为Reader
   *
   * @param file 文件
   * @return BufferedReader对象
   */
  public static BufferedReader getBOMReader(File file) {
    return IOUtils.getReader(getBOMInputStream(file));
  }

  /**
   * 获得一个文件读取器
   *
   * @param file 文件
   * @return BufferedReader对象
   * @throws IoException IO异常
   */
  public static BufferedReader getUtf8Reader(File file) throws IoException {
    return getReader(file, CharsetUtils.CHARSET_UTF_8);
  }

  /**
   * 获得一个文件读取器
   *
   * @param path 文件路径
   * @return BufferedReader对象
   * @throws IoException IO异常
   */
  public static BufferedReader getUtf8Reader(String path) throws IoException {
    return getReader(path, CharsetUtils.CHARSET_UTF_8);
  }

  /**
   * 获得一个文件读取器
   *
   * @param file 文件
   * @param charsetName 字符集
   * @return BufferedReader对象
   * @throws IoException IO异常
   */
  public static BufferedReader getReader(File file, String charsetName) throws IoException {
    return IOUtils.getReader(getInputStream(file), charsetName);
  }

  /**
   * 获得一个文件读取器
   *
   * @param file 文件
   * @param charset 字符集
   * @return BufferedReader对象
   * @throws IoException IO异常
   */
  public static BufferedReader getReader(File file, Charset charset) throws IoException {
    return IOUtils.getReader(getInputStream(file), charset);
  }

  /**
   * 获得一个文件读取器
   *
   * @param path 绝对路径
   * @param charsetName 字符集
   * @return BufferedReader对象
   * @throws IoException IO异常
   */
  public static BufferedReader getReader(String path, String charsetName) throws IoException {
    return getReader(file(path), charsetName);
  }

  /**
   * 获得一个文件读取器
   *
   * @param path 绝对路径
   * @param charset 字符集
   * @return BufferedReader对象
   * @throws IoException IO异常
   */
  public static BufferedReader getReader(String path, Charset charset) throws IoException {
    return getReader(file(path), charset);
  }

  /**
   * 根据压缩包中的路径构建目录结构，在Win下直接构建，在Linux下拆分路径单独构建
   *
   * @param outFile 最外部路径
   * @param fileName 文件名，可以包含路径
   * @return 文件或目录
   */
  private static File buildFile(File outFile, String fileName) {
    // 替换Windows路径分隔符为Linux路径分隔符，便于统一处理
    fileName = fileName.replace('\\', '/');
    if (false == isWindows()
        // 检查文件名中是否包含"/"，不考虑以"/"结尾的情况
        && fileName.lastIndexOf(CharUtils.SLASH, fileName.length() - 2) > 0) {
      // 在Linux下多层目录创建存在问题，/会被当成文件名的一部分，此处做处理
      // 使用/拆分路径（zip中无\），级联创建父目录
      final List<String> pathParts = StrUtils.split(fileName, "/", false, true);
      final int lastPartIndex = pathParts.size() - 1; // 目录个数
      for (int i = 0; i < lastPartIndex; i++) {
        // 由于路径拆分，slip不检查，在最后一步检查
        outFile = new File(outFile, pathParts.get(i));
      }
      //noinspection ResultOfMethodCallIgnored
      outFile.mkdirs();
      // 最后一个部分如果非空，作为文件名
      fileName = pathParts.get(lastPartIndex);
    }
    return new File(outFile, fileName);
  }
}
